Redis Cluster 分片与高可用
系统梳理 Redis Cluster 的 Hash Slot、Hash Tag、MOVED、ASK、Cluster Bus、Gossip、PFAIL、FAIL、副本选举、槽位迁移、同槽约束与 Go 客户端实践。
第 17 章:Redis Cluster 分片与高可用
1. 本章定位
Redis Sentinel 解决的是单个主从复制组的自动故障转移问题,但数据仍主要由一个主节点承载。Redis Cluster 在主从复制和自动故障转移的基础上,引入了数据分片,使多个主节点可以同时承载不同的数据和请求。
理解 Redis Cluster,可以先建立下面这条主线:
Key → Hash Slot → Master → Replica
客户端先根据 Key 计算 Hash Slot,再根据槽位拓扑找到负责该槽的主节点。主节点负责读写,副本异步复制数据,并在主节点故障后参与选举和接管槽位。
Redis Cluster 的核心目标是横向扩展、高吞吐和一定程度的故障可用性,而不是跨分片事务或强一致性。它没有代理层,正常情况下客户端直接访问目标节点;多 Key 命令、事务和 Lua 脚本只有在相关 Key 属于同一槽时才能执行。Redis Cluster 仅支持数据库 0。(Redis)
2. 学习目标
学完本章后,应能够:
- 根据普通 Key 和带 Hash Tag 的 Key 判断其 Hash Slot。
- 解释 Redis Cluster 为什么使用固定的 16384 个槽,而不是经典一致性哈希环。
- 准确区分
MOVED和ASK重定向,并说明客户端应该如何处理。 - 解释 Cluster Bus、Gossip、
PFAIL、FAIL和副本选举之间的关系。 - 分析多数主节点不可达、槽位失去覆盖、主从同时故障时的集群行为。
- 解释为什么 Redis Cluster 已确认的写入仍可能丢失,以及
WAIT能改善什么、不能保证什么。 - 设计满足同槽约束的 Key,并使用 go-redis 正确执行事务、Lua 和批量请求。
- 制定扩容、缩容、槽位迁移和热点槽治理方案。
2.1 本章边界与跳转
本章是 Redis Cluster 分片与分片级高可用的主章节。Sentinel 只解决单个主从复制组的自动故障转移,见[第 16 章](/blog/tech/Redis/16.Sentinel 高可用/);复制确认和数据丢失窗口见第 15 章;多 Key、事务、Lua 和 Pipeline 的同槽约束在[第 9 章](/blog/tech/Redis/09.Pipeline、事务、WATCH、Lua 与 Functions/)继续展开;容量规划、热 Slot 和水平分片的综合面试回答见第 20 章。
3. 核心概念
3.1 Redis Cluster 的目标与拓扑
Redis Cluster 主要解决两个问题:
- 数据容量和吞吐横向扩展:不同主节点持有不同槽位,多个主节点可以并行处理请求。
- 分片级高可用:每个主节点可以配置一个或多个副本,主节点故障后由副本接管。
一个典型的六节点集群如下:
Shard A: Master A ← Replica A1
Shard B: Master B ← Replica B1
Shard C: Master C ← Replica C1
Redis 7.0 之后,官方通过 CLUSTER SHARDS 明确定义了 Shard:一组服务相同槽位、互相复制的节点,其中同一时间最多有一个主节点,可以有零个或多个副本。(Redis)
Redis Cluster 不在节点之间代理客户端命令。客户端即使连接到了错误节点,也会收到 MOVED 或 ASK,随后访问真正负责该槽位的节点。因此,生产应用必须使用 Cluster-Aware Client,而不是把普通单机客户端随意连接到某个集群节点。(Redis)
3.2 16384 个 Hash Slot
Redis Cluster 将整个 Key 空间划分为 16384 个槽,编号范围为:
0 ~ 16383
普通 Key 的槽位计算公式为:
slot = CRC16(key) mod 16384
由于 16384 等于 2^14,实现上也可以理解为使用 CRC16 结果的低 14 位:
slot = CRC16(key) & 0x3FFF
计算 CRC16 的时间复杂度是 O(L),其中 L 是参与计算的 Key 或 Hash Tag 字节数;计算出槽号后,通过槽位表查找目标节点可以近似视为 O(1)。(Redis)
为什么是 16384,而不是 65536?
面试中不宜只背“因为心跳包太大”这一句话。更完整的解释是:
- 槽位必须足够多,才能提供较细的迁移和负载均衡粒度。
- 槽位也不能无限多,因为节点需要传播自身负责槽位的位图。
- 16384 个槽对应 16384 位,即原始位图约为 2048 字节;若使用 65536 个槽,位图会扩大到约 8192 字节。
- 16384 已经能支持很大的主节点数量,实际集群通常远小于这个理论上限。
- 固定槽位数使服务端和客户端都可以维护结构简单、查询高效的槽位映射表。
Cluster 心跳消息中确实包含节点负责槽位的位图,因此槽数会直接影响集群元数据传播成本。(Redis)
3.3 为什么不使用经典一致性哈希
Redis Cluster 官方明确说明它不使用一致性哈希,而是采用固定 Hash Slot。(Redis)
两种模型的主要差异如下:
| 维度 | 经典一致性哈希 | Redis Cluster Hash Slot |
|---|---|---|
| Key 定位 | Key 映射到哈希环上的节点或虚拟节点 | Key 先映射到固定槽,再由槽映射到节点 |
| 节点变化 | 环结构变化,部分 Key 重新映射 | 显式移动部分槽位 |
| 客户端元数据 | 节点环、虚拟节点列表 | 16384 槽到节点的映射 |
| 迁移控制 | 通常由环变化决定范围 | 运维可以精确选择迁移哪些槽 |
| 多 Key 共置 | 需要额外的路由设计 | 原生 Hash Tag |
| 运维代价 | 依赖具体实现 | 槽位迁移需要复制实际 Key |
Hash Slot 增加了一层间接映射:
key → slot → node
这使扩容时不需要改变 key → slot 的结果,只需要改变 slot → node 的归属。客户端可以缓存一个固定范围的槽位表,故障转移和迁槽也可以通过更新槽位所有权传播。
一致性哈希并不是“错误方案”。它更适合客户端分片、缓存节点环和允许节点增减时自动重映射的场景。Redis Cluster 选择槽位,是为了获得更明确的槽位所有权、可控迁移和统一的客户端协议。
3.4 Hash Tag
Redis Cluster 要求多 Key 命令涉及的所有 Key 属于同一槽。Hash Tag 可以强制多个 Key 使用同一段内容计算槽位:
user:{42}:profile
user:{42}:quota
user:{42}:version
这三个 Key 都只对 42 计算 CRC16,因此属于同一个槽。
Hash Tag 的匹配规则不是“取任意一对大括号”,而是寻找第一对有效的 {...}:
- 必须存在
{。 - 其右侧必须存在
}。 - 两者之间至少有一个字符。
- 只使用第一对有效括号中的内容。
例如:
foo{bar}{other} → 使用 bar
foo{}bar → 对整个 Key 计算
foo{{bar}} → 使用 {bar
官方规范给出了完整的 Hash Tag 解析规则。Redis 8.0 还针对能够确定为单槽的 KEYS、SCAN、SORT 模式增加了单槽扫描优化。(Redis)
Hash Tag 的代价
Hash Tag 解决了数据共置问题,但也可能破坏分片效果:
{all-users}:profile:1
{all-users}:profile:2
{all-users}:profile:3
以上所有 Key 都进入同一槽,相当于主动放弃水平分片。
更合理的原则是:
- 按需要原子操作的最小业务实体打 Tag。
- 用户维度操作可使用
{userID}。 - 订单维度操作可使用
{orderID}。 - 不要直接使用全局常量或超大租户 ID 聚合全部数据。
- 如果业务 ID 可能包含
{、},应先进行稳定编码,避免意外改变 Tag。
3.5 MOVED 与 ASK
| 对比项 | MOVED | ASK |
|---|---|---|
| 含义 | 槽位已经由另一个节点负责 | 槽位正在迁移,本次请求暂时去另一个节点 |
| 持续性 | 通常是长期或永久映射变化 | 仅对当前被重定向的命令有效 |
| 客户端槽表 | 应更新或重新加载 | 不应永久修改 |
| 后续请求 | 直接访问新节点 | 仍先访问原槽位节点 |
| 附加命令 | 无 | 向目标节点先发送 ASKING |
| 常见场景 | 故障转移完成、迁槽完成、槽表过期 | 经典在线迁槽过程中 |
MOVED
典型响应:
MOVED 12182 10.0.0.12:6379
表示槽 12182 现在由 10.0.0.12:6379 负责。
客户端可以立即修正该槽映射,但成熟客户端通常还会异步重新加载完整拓扑,以免连续遭遇大量 MOVED。
ASK
典型响应:
ASK 12182 10.0.0.12:6379
客户端应在同一条目标连接上依次执行:
ASKING
原始命令
ASKING 设置的是一次性连接状态,只允许下一条命令临时访问正在导入的槽。客户端不能因为一次 ASK 就永久修改槽表,否则后续尚未迁走的 Key 可能被错误地发送到目标节点。(Redis)
3.6 Cluster-Aware Client
一个成熟的集群客户端通常执行以下流程:
- 使用多个 Seed 地址连接任一可用节点。
- 读取集群拓扑和槽位分配。
- 在本地缓存
slot → node映射。 - 根据命令涉及的 Key 计算槽位。
- 从目标节点的连接池取得连接并执行命令。
- 遇到
MOVED时更新或刷新拓扑。 - 遇到
ASK时向目标节点发送ASKING + 原命令。 - 遇到网络错误、故障转移或拓扑抖动时进行有限重试和退避。
Redis 7.0+ 提供更具扩展性的 CLUSTER SHARDS,旧客户端也可能使用 CLUSTER SLOTS。CLUSTER SHARDS 适合客户端在启动时建立拓扑,并在收到 MOVED 后重新加载。(Redis)
Cluster 客户端必须能访问拓扑中返回的每一个节点地址。只让客户端访问一个入口地址,而节点返回的 IP、端口不可达,会导致客户端收到 MOVED 后连接失败。
原生 Redis Cluster 对 NAT、端口重映射和错误的节点公告地址比较敏感。工程上的本质要求是:
- 客户端端口对应用可达。
- Cluster Bus 端口在所有节点之间可达。
- 节点公告的 IP、主机名和端口必须与实际网络一致。(Redis)
3.7 Cluster Bus、Gossip、PFAIL 与 FAIL
每个 Redis Cluster 节点通常有两个端口:
- 客户端命令端口:处理
GET、SET等命令。 - Cluster Bus 端口:节点之间进行故障检测、拓扑传播、选举授权和配置更新。
Cluster Bus 使用二进制协议,客户端不应连接该端口。(Redis)
心跳消息包含:
- Node ID。
currentEpoch和configEpoch。- 节点角色和状态。
- 节点负责槽位的位图。
- 客户端端口和 Cluster Bus 端口。
- 发送方观察到的集群状态。
- 少量随机节点的 Gossip 信息。
Gossip 不是每次广播所有节点的完整状态,而是抽取一部分节点信息随心跳传播,在控制消息大小的同时实现最终扩散。(Redis)
PFAIL
PFAIL 表示 Possible Failure,是某个节点的本地怀疑。
当节点向另一个节点发送 Ping,超过 cluster-node-timeout 仍未收到有效响应时,就可能将对方标记为 PFAIL。
单个节点的网络抖动、CPU 卡顿或连接异常,不应立即触发主从切换,因此 PFAIL 本身不会直接启动故障转移。
FAIL
FAIL 表示故障已经得到集群层面的确认。
当某个节点收集到多数主节点在有效时间窗口内对目标节点的 PFAIL 或 FAIL 报告后,可以将其提升为 FAIL,并广播专门的 FAIL 消息。(Redis)
这个过程使用了一种较弱的多数派确认,但它不是 Raft 那样对每条业务写入进行共识复制。这里达成的是“某节点是否故障”的判断,而不是业务数据日志的一致提交。
3.8 主从复制与故障转移
当主节点被确认进入 FAIL 状态后,其副本可以参与选举。
大致流程如下:
- 主节点被标记为
FAIL。 - 副本检查自己是否足够新、主从断开时间是否在允许范围内。
- 多个副本根据复制偏移量计算 Rank。
- 数据更新程度较高的副本通常更早发起选举。
- 候选副本增加
currentEpoch,向主节点请求投票。 - 获得多数主节点投票后,副本晋升为主节点。
- 新主节点获得更高的
configEpoch。 - 新主节点通过心跳宣布自己接管原主节点的槽位。
- 客户端通过
MOVED或拓扑刷新逐渐切换到新主节点。 - 旧主节点恢复并发现更高 Epoch 后,通常会降级为新主节点的副本。
副本选举需要多数主节点授权;复制偏移量更靠前的副本会获得较低 Rank,从而更早尝试选举。最终由更高 configEpoch 的槽位声明胜出。(Redis)
因此,故障恢复时间不应简单等同于 cluster-node-timeout。真实恢复时间还包含:
故障发现
+ PFAIL/FAIL 传播
+ 副本选举延迟
+ 拓扑传播
+ 客户端重连和重试
3.9 多数派、可用性与一致性边界
Redis Cluster 能够在以下条件下继续提供主要服务:
- 多数主节点仍可互相通信。
- 每个不可达的主节点都有可用且具备选举资格的副本。
- 槽位能够被新的主节点继续覆盖。
当多数主节点不可达时,集群无法获得故障转移所需多数票,通常会进入失败状态。官方文档明确指出,较大故障,例如多数主节点不可用,会导致集群不可用。(Redis)
默认配置还包括:
cluster-require-full-coverage yes:部分槽无人负责时,集群停止接受写入。cluster-allow-reads-when-down no:集群处于失败状态时,默认停止提供流量。
可以调整这些配置以允许部分槽继续服务或在失败状态下读取,但这会改变可用性和一致性边界,不能把它理解为恢复了安全的多数派写入能力。(Redis)
Redis Cluster 不是强一致系统
主节点默认使用异步复制:
客户端 → 主节点写入
主节点 → 向客户端返回 OK
主节点 → 异步传播给副本
如果主节点返回 OK 后立即故障,而该写入尚未到达副本,随后一个旧副本被提升为主节点,这次已经确认的写入就会丢失。(Redis)
网络分区时风险更明显:
- 客户端与少数派主节点处在一起。
- 少数派主节点可能在一段时间内继续接收写入。
- 多数派将其副本提升为新主节点。
- 分区恢复后,旧主节点的数据被新主节点覆盖。
- 少数派窗口内的写入丢失。
少数派主节点在超过 cluster-node-timeout、无法感知多数主节点后会停止接受写入,因此风险窗口有上限,但并不为零。(Redis)
WAIT 可以要求写入在返回前到达一定数量的副本,从而降低数据丢失概率,但即使使用 WAIT,Redis Cluster 仍不提供严格强一致性:复杂故障条件下,未收到该写入的副本仍可能当选。(Redis)
3.10 多 Key、事务与 Lua 的同槽限制
以下操作通常要求所有 Key 属于同一槽:
MGET、MSET。- 集合交集、并集、差集等多 Key 命令。
MULTI/EXEC事务中的所有带 Key 命令。WATCH涉及的 Key。- Lua 脚本访问的所有 Key。
- Redis Functions 中需要共同执行的 Key。
例如:
MGET user:{42}:profile user:{42}:quota
可以执行,因为两个 Key 同槽。
而:
MGET user:42:profile user:99:profile
通常返回:
CROSSSLOT Keys in request don't hash to the same slot
Redis Cluster 不提供跨分片的原子提交协议。即使客户端可以把多条命令分别发到不同分片,也只是客户端聚合结果,不是分布式事务。(Redis)
需要特别区分:
- 多 Key 命令:一条服务端命令,必须同槽。
- 事务或 Lua:要求原子执行,因此必须同槽。
- 普通 Pipeline:只是批量发送和减少网络往返,客户端可以按节点拆分,但不提供跨节点原子性。
3.11 在线扩缩容、槽位迁移与热点槽
经典槽位迁移
传统迁槽的核心状态为:
源节点:MIGRATING
目标节点:IMPORTING
流程通常是:
- 将目标节点上的槽设置为
IMPORTING。 - 将源节点上的槽设置为
MIGRATING。 - 使用
CLUSTER GETKEYSINSLOT找出槽内 Key。 - 使用
MIGRATE将 Key 从源节点复制到目标节点。 - 重复直到该槽没有剩余 Key。
- 使用
CLUSTER SETSLOT ... NODE ...正式切换槽位归属。 - 将新槽位所有权传播给集群其他节点。
迁移期间:
- 源节点上的已有 Key 仍由源节点处理。
- 如果源节点发现请求的 Key 已经迁走,会返回
ASK。 - 目标节点只有在客户端先发送
ASKING时,才临时处理正在导入槽的请求。(Redis)
迁移槽的本质是迁移槽内的全部 Key。槽数量相同,不代表迁移成本相同;一个包含 BigKey 的槽,迁移成本可能远大于包含大量小 Key 的槽。官方规范也明确提醒,在低延迟要求下迁移 BigKey 并不明智。(Redis)
Redis 8.4+ 原子槽迁移
Redis Open Source 8.4 增加了 CLUSTER MIGRATION,可在目标主节点发起槽范围导入,并查询或取消迁移任务:
CLUSTER MIGRATION IMPORT 0 1000
CLUSTER MIGRATION STATUS ALL
这里的“原子”主要强调最终槽位所有权的交接,而不是所有数据瞬间完成复制。后台仍需要复制槽数据,并在最终交接时协调写入暂停和复制追平。
迁移过程中,未拥有槽、正在导入或裁剪的 Key 可能暂时不出现在 SCAN、DBSIZE、CLUSTER GETKEYSINSLOT 等结果中,INFO KEYSPACE 仍会反映实际 Key 数量。(Redis)
扩容
增加空主节点
→ 加入集群
→ 从现有主节点迁入一部分槽
→ 检查槽覆盖与负载
→ 为新主节点补充副本
缩容
把目标主节点的全部槽迁到其他主节点
→ 确认节点不再负责任何槽
→ 删除节点
主节点必须先清空槽位,才能安全从集群移除。(Redis)
热点槽与数据倾斜
槽数量平均不代表负载平均。常见原因包括:
- Hash Tag 粒度过粗。
- 单个 HotKey 请求量极高。
- 单个 BigKey 占用大量内存或网络。
- 不同槽内 Key 数量相近,但 Value 大小差异巨大。
- 时间型 Key 或顺序型业务集中访问近期数据。
- 少数大租户远大于其他租户。
- 全局排行榜、全局计数器等单 Key 结构天然只能位于一个槽。
Redis 8.2+ 提供 CLUSTER SLOT-STATS,可以按槽查看 Key 数、CPU、内存和网络指标。其中除 KEY-COUNT 外的指标需要开启 cluster-slot-stats-enabled。该命令只返回当前 Shard 的槽统计,分析整个集群时要查询所有主节点。(Redis)
4. 命令与 Go 使用方法
4.1 redis-cli 操作
连接 Cluster
redis-cli -c -h 127.0.0.1 -p 7000
-c 表示自动跟随 MOVED 和 ASK 重定向。
官方示例说明,redis-cli -c 会根据节点返回的重定向访问目标节点,而成熟客户端通常还会缓存完整槽位映射。(Redis)
查看集群状态
CLUSTER INFO
CLUSTER NODES
CLUSTER SHARDS
Redis 7.0 之前或需要兼容旧客户端时,可查看:
CLUSTER SLOTS
计算槽位
CLUSTER KEYSLOT user:{42}:profile
CLUSTER KEYSLOT user:{42}:quota
CLUSTER KEYSLOT user:{99}:profile
前两个结果相同,第三个通常不同。
验证同槽命令
MSET user:{42}:profile '{"name":"alice"}' user:{42}:quota 100
MGET user:{42}:profile user:{42}:quota
跨槽示例:
MGET user:{42}:profile user:{99}:profile
预期返回:
(error) CROSSSLOT Keys in request don't hash to the same slot
观察 MOVED
不使用 -c 连接:
redis-cli -h 127.0.0.1 -p 7000 GET user:{42}:profile
可能返回:
(error) MOVED 8000 127.0.0.1:7001
手工处理 ASK
迁槽期间收到:
ASK 8000 127.0.0.1:7002
连接目标节点,并在同一连接上执行:
ASKING
GET user:{42}:profile
添加主节点
redis-cli --cluster add-node \
127.0.0.1:7006 \
127.0.0.1:7000
新主节点加入后最初没有槽,也不会自动分担已有数据,需要继续执行迁槽或 Rebalance。(Redis)
重新分配槽位
交互式:
redis-cli --cluster reshard 127.0.0.1:7000
自动化示意:
redis-cli --cluster reshard 127.0.0.1:7000 \
--cluster-from <source-node-id> \
--cluster-to <target-node-id> \
--cluster-slots 1000 \
--cluster-yes
执行完成后检查:
redis-cli --cluster check 127.0.0.1:7000
官方 redis-cli --cluster reshard 会发现其余节点并迁移指定槽位。该过程可以在线执行,但“在线”不等于完全没有延迟波动。(Redis)
删除节点
redis-cli --cluster del-node \
127.0.0.1:7000 \
<node-id>
删除主节点前必须迁走其全部槽位。
查看热点槽
Redis 8.2+:
CLUSTER SLOT-STATS ORDERBY CPU-USEC LIMIT 10 DESC
CLUSTER SLOT-STATS ORDERBY MEMORY-BYTES LIMIT 10 DESC
CLUSTER SLOT-STATS ORDERBY NETWORK-BYTES-OUT LIMIT 10 DESC
Redis 8.4+ 原子迁槽
在目标主节点执行:
CLUSTER MIGRATION IMPORT 0 1000
CLUSTER MIGRATION STATUS ALL
取消任务:
CLUSTER MIGRATION CANCEL ID <task-id>
这是 @admin、@slow、@dangerous 命令,生产执行前必须确认版本、备份、容量、复制延迟和回滚路径。(Redis)
4.2 Go:创建 ClusterClient
官方 Go 客户端通过 redis.NewClusterClient 创建集群客户端,并接收一个或多个 Seed 地址。(Redis)
package rediscluster
import (
"context"
"errors"
"fmt"
"time"
"github.com/redis/go-redis/v9"
)
func NewClient(addrs []string) *redis.ClusterClient {
return redis.NewClusterClient(&redis.ClusterOptions{
Addrs: addrs,
Username: "app",
Password: "replace-with-secret",
// MOVED、ASK 和部分网络错误的最大重定向次数。
MaxRedirects: 3,
DialTimeout: 2 * time.Second,
ReadTimeout: 800 * time.Millisecond,
WriteTimeout: 800 * time.Millisecond,
PoolTimeout: 1 * time.Second,
// 注意:ClusterClient 中 PoolSize 是每个节点的连接池大小。
PoolSize: 32,
// 让命令 Context 的截止时间参与超时控制。
ContextTimeoutEnabled: true,
})
}
// Ping 只能作为基本连通性检查,不能证明所有 Shard 都健康。
func Ping(parent context.Context, rdb *redis.ClusterClient) error {
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel()
if err := rdb.Ping(ctx).Err(); err != nil {
return fmt.Errorf("ping redis cluster: %w", err)
}
return nil
}
func GetUserProfile(
parent context.Context,
rdb *redis.ClusterClient,
userID string,
) (value string, found bool, err error) {
ctx, cancel := context.WithTimeout(parent, 1200*time.Millisecond)
defer cancel()
key := fmt.Sprintf("user:{%s}:profile", userID)
value, err = rdb.Get(ctx, key).Result()
switch {
case err == nil:
return value, true, nil
case errors.Is(err, redis.Nil):
return "", false, nil
default:
return "", false, fmt.Errorf("get %q: %w", key, err)
}
}
工程注意事项:
ClusterClient可以被多个 Goroutine 并发复用,不要为每个请求创建新客户端。- 客户端应在进程启动时创建,在进程退出时统一
Close。 PoolSize和MaxActiveConns是每个集群节点的配置,不是整个集群的总连接数。MaxRedirects默认用于处理网络错误及MOVED、ASK,不宜无限增大,否则故障期间可能形成重试风暴。RouteByLatency和RouteRandomly会启用副本读取,只适用于能够接受旧数据的场景。(GitHub)
4.3 Go:通过 Hash Tag 保证事务同槽
func UpdateUserAtomically(
parent context.Context,
rdb *redis.ClusterClient,
userID string,
name string,
) error {
ctx, cancel := context.WithTimeout(parent, 1500*time.Millisecond)
defer cancel()
profileKey := fmt.Sprintf("user:{%s}:profile", userID)
versionKey := fmt.Sprintf("user:{%s}:version", userID)
_, err := rdb.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
pipe.HSet(ctx, profileKey, "name", name)
pipe.Incr(ctx, versionKey)
return nil
})
if err != nil {
return fmt.Errorf("update user %s atomically: %w", userID, err)
}
return nil
}
TxPipelined 会使用 MULTI/EXEC,因此这里的两条命令在目标 Shard 上原子执行。但它并没有产生跨分片事务能力;如果两个 Key 不同槽,当前 go-redis ClusterClient 会返回跨槽错误。
4.4 Go:跨槽普通 Pipeline
普通 Pipeline 中的命令可以属于不同槽。当前 go-redis ClusterClient 会根据目标节点对命令分组,并并发发送到不同节点,但这不是一个原子操作。(GitHub)
func BatchSetIndependentUsers(
parent context.Context,
rdb *redis.ClusterClient,
) error {
ctx, cancel := context.WithTimeout(parent, 1500*time.Millisecond)
defer cancel()
pipe := rdb.Pipeline()
cmdA := pipe.Set(
ctx,
"user:{42}:last_seen",
time.Now().Unix(),
24*time.Hour,
)
cmdB := pipe.Set(
ctx,
"user:{99}:last_seen",
time.Now().Unix(),
24*time.Hour,
)
_, execErr := pipe.Exec(ctx)
if execErr == nil {
return nil
}
var errs []error
errs = append(errs, fmt.Errorf("pipeline exec: %w", execErr))
if err := cmdA.Err(); err != nil {
errs = append(errs, fmt.Errorf("user 42: %w", err))
}
if err := cmdB.Err(); err != nil {
errs = append(errs, fmt.Errorf("user 99: %w", err))
}
return errors.Join(errs...)
}
必须考虑以下结果:
Shard A 命令成功
Shard B 命令失败
Pipeline 整体返回错误
此时不能不加区分地重放整个 Pipeline。对于 SET 这类幂等命令,重试通常较容易控制;对于 INCR、LPUSH、扣减库存等非幂等操作,网络错误可能发生在“服务端已经执行、客户端没有收到响应”之后,盲目重试可能造成重复执行。
5. 典型业务场景
| 业务场景 | 适合度 | 数据与一致性特点 | 主要风险 | 可选替代方案 |
|---|---|---|---|---|
| 海量用户 Session、Token | 高 | 用户之间基本独立,允许按用户分片 | 热门账号、Value 过大、TTL 集中 | 本地缓存加 Redis、专用会话服务 |
| 用户维度限流 | 高 | 每个用户独立;多个限流 Key 可使用 {userID} | 大租户热点、过度聚合 Tag | 网关本地限流、专用限流系统 |
| 购物车、用户配置 | 较高 | 同一用户相关 Key 可同槽 | 单用户数据过大、跨用户操作受限 | Hash 单 Key、关系数据库 |
| 分租户缓存 | 条件适合 | 租户之间可分片 | 超大租户形成热点槽 | 大租户二级分片、独立集群 |
| 全局排行榜单个 ZSet | 较低 | 单个 Key 只能位于一个槽 | 单分片 CPU、内存和网络瓶颈 | 分区排行榜后合并、分析数据库 |
| 全局精确计数器 | 较低 | 单 Key 热点,无法自动拆分 | 热 Key、主节点瓶颈 | 分片计数后汇总、流处理系统 |
| 严格资金账本 | 不适合充当权威存储 | 要求强一致、审计和事务 | 已确认写入仍可能丢失 | 关系数据库、共识型数据库 |
| 跨租户原子事务 | 不适合 | Key 天然跨槽 | 无跨分片事务 | 数据库事务、消息/Saga |
| 大规模缓存集群 | 高 | 可接受短暂失败和缓存重建 | 雪崩、重试风暴、热槽 | Cluster 配合本地缓存和降级 |
Redis Cluster 最适合“数据可以自然按业务实体分区、单实体操作能够局限在一个槽、可接受异步复制一致性”的场景。
6. 底层实现
6.1 核心数据结构
概念上,一个 Cluster 节点需要维护:
-
节点表 保存已知节点的 Node ID、地址、角色、主从关系、存活状态和 Epoch。
-
16384 槽所有权表 每个槽指向当前负责它的主节点,便于
O(1)定位槽所有者。 -
节点槽位位图 表示某节点负责哪些槽,用于心跳和拓扑传播。
-
主从复制状态 包括复制偏移量、主从连接状态和副本新鲜度。
-
槽内 Key 索引 除主 Keyspace 字典外,还需要能够快速查找某个槽中的 Key,以支持
CLUSTER GETKEYSINSLOT和迁槽。 -
Epoch 信息
currentEpoch用于选举轮次,configEpoch用于判断不同槽位所有权声明的新旧。
当两个节点同时声称拥有同一槽时,更高 configEpoch 的声明获胜。旧节点恢复后会通过心跳或 UPDATE 消息学习最新槽位所有权。(Redis)
6.2 正常命令执行路径
客户端解析命令 Key
→ 计算 CRC16 和槽位
→ 查本地槽位映射
→ 从目标节点连接池获取连接
→ 网络发送命令
→ 目标主节点执行命令
→ 返回结果
时间主要由以下部分组成:
Key 哈希计算
+ 客户端路由
+ 连接池等待
+ 网络 RTT
+ Redis 命令执行
+ 响应传输
Cluster 不会让一次普通 GET 同时访问多个主节点。分片提高的是整个集群的聚合吞吐和容量,不会自动降低单条命令的网络延迟。
6.3 重定向路径
MOVED
客户端槽表过期
→ 请求到旧节点
→ 收到 MOVED
→ 访问新节点
→ 更新或重新加载拓扑
相较正常路径,至少增加一次错误响应和一次额外网络访问。
ASK
请求到迁出节点
→ Key 已迁走
→ 收到 ASK
→ 连接迁入节点
→ ASKING
→ 原始命令
ASK 还可能增加连接切换和两条命令发送成本,因此大规模迁槽期间,尾延迟通常会高于稳定状态。
6.4 故障检测和选举不是业务命令共识
Cluster Bus 后台持续执行:
Ping/Pong
→ Gossip 状态传播
→ PFAIL
→ 多数主节点故障报告
→ FAIL
→ 副本选举
→ 新 configEpoch
→ 槽位所有权传播
这些是后台控制面任务,不是每条业务命令的同步提交阶段。因此:
- 正常写入不等待多数主节点确认。
- 正常写入也不等待副本持久化后再返回。
- 故障检测的多数派不能等同于业务数据的强一致多数派复制。
这正是 Redis Cluster 能保持较低写延迟,但存在非零 RPO 的原因。
6.5 迁槽成本分析
| 操作 | 命令本身 | 网络与数据成本 | 后台或阻塞影响 |
|---|---|---|---|
CLUSTER SETSLOT | 修改槽状态,成本较低 | 传播少量元数据 | 等待 Gossip 或主动通知 |
CLUSTER GETKEYSINSLOT | 查找槽内部分 Key | 返回 Key 名称 | 批次过大增加响应体 |
MIGRATE | 序列化和删除 Key | 传输完整 Key 与 Value | BigKey 可造成明显延迟 |
redis-cli --cluster reshard | 编排多个命令 | 传输所有待迁 Key | 迁移越久,ASK 窗口越长 |
CLUSTER MIGRATION | 发起任务的命令按槽范围处理 | 后台复制槽数据 | 最终交接可能短暂停写 |
因此,不能只根据“迁移 1000 个槽”估算时间。更关键的指标是:
- 槽内 Key 数量。
- 数据总字节数。
- BigKey 分布。
- 源节点和目标节点 CPU。
- 主从复制延迟。
- 网络带宽。
- 迁移期间写入速率。
6.6 为什么使用这些结构
固定槽位表
优点:
- 路由简单。
- 客户端容易缓存。
- 故障转移只需改变槽所有者。
- 运维可以精确选择迁移范围。
代价:
- 必须管理槽迁移状态。
- 负载均衡粒度受槽内数据分布影响。
- 槽均匀不代表 QPS、内存均匀。
位图
优点:
- 表达槽集合紧凑。
- 可随心跳传播。
- 集合更新和判断高效。
代价:
- 槽数越多,心跳固定负担越大。
Gossip
优点:
- 无单独中心协调节点。
- 节点能够逐步发现拓扑和故障。
- 单次消息不必携带所有节点完整状态。
代价:
- 状态传播存在延迟。
- 网络抖动时不同节点可能短暂持有不同观察。
- 节点越多、
NODE_TIMEOUT越小,控制面消息越多。(Redis)
异步复制
优点:
- 主节点写入延迟较低。
- 不需要每条写入等待副本。
- 聚合吞吐较高。
代价:
- 已确认写入仍有丢失窗口。
- 副本读取可能过期。
- 故障转移具有非零 RPO。
6.7 版本差异
| 能力 | 版本 | 说明 |
|---|---|---|
| Redis Cluster 基础能力 | Redis 3.0+ | 固定槽、重定向、主从故障转移 |
CLUSTER SHARDS | Redis 7.0+ | 更可扩展的 Shard 拓扑描述,替代 CLUSTER SLOTS |
| 单槽 Glob 模式优化 | Redis 8.0+ | 特定 Hash Tag 模式的 SCAN 等只扫描目标槽 |
CLUSTER SLOT-STATS | Redis 8.2+ | 按槽观察 Key、CPU、内存和网络 |
CLUSTER MIGRATION | Redis 8.4+ | 原子槽迁移任务、状态查询与取消 |
升级前应确认服务端版本、客户端版本及托管 Redis 产品是否支持相应命令,不能仅根据本地 redis-cli 版本判断。
7. 高性能、高并发、高可用分析
7.1 高性能
Redis Cluster 的性能收益主要来自多个主节点并行:
Master A 处理槽 0~5000
Master B 处理槽 5001~10000
Master C 处理槽 10001~16383
当 Key 分布和请求分布较均匀时,CPU、内存和网络压力可以分散到多个节点。
但要注意:
- Cluster 提升的是聚合吞吐,不是单命令天然加速。
- 单个 Key 永远只由一个主节点处理。
- 一个热门槽仍然只能由一个主节点负责写入。
- 单个全局 ZSet、Hash 或 List 无法被 Cluster 自动拆分。
- 频繁
MOVED说明客户端槽表长期不准确。 - 迁槽期间的
ASK会增加额外 RTT。 - Pipeline 跨节点后,每个节点仍有独立网络往返。
优化重点应包括:
- 让客户端直接命中正确节点。
- 控制连接池等待。
- 合理使用按节点拆分的 Pipeline。
- 避免过粗 Hash Tag。
- 在迁槽前治理 BigKey。
- 同时按槽观察 Key 数、字节数、CPU 和网络,而不是只看槽数量。
7.2 高并发
多个主节点允许不同槽上的命令并行执行,例如:
user:{42}:* → Shard A
user:{99}:* → Shard B
order:{1001}:* → Shard C
但高并发场景容易出现以下问题:
-
连接数按节点放大
PoolSize=32、六个节点时,连接数量可能达到数百量级;多个应用实例会进一步放大。 -
热点槽打穿单节点 整体集群 CPU 不高,不代表热点主节点没有达到瓶颈。
-
故障期间重试放大 客户端同时收到超时、
MOVED、连接重置,若每层都重试,可能产生乘法效应。 -
跨槽批量部分成功 Pipeline 的一部分节点成功,另一部分失败,业务必须能够识别并补偿。
-
拓扑刷新惊群 大量客户端同时感知故障并刷新槽表,可能给集群增加额外控制流量。
建议:
- 每个业务请求只设置一层明确的重试预算。
- 使用指数退避和随机抖动。
- 对非幂等写引入业务幂等 ID。
- 监控每个节点的连接池等待和活跃连接数。
- 限制单次 Pipeline 大小,避免大批量响应长时间占用连接。
- 对热点业务实施应用级二次分片。
7.3 高可用
Redis Cluster 的高可用依赖:
- 主节点多数派仍然可达。
- 故障主节点存在合格副本。
- 主副本分布在不同故障域。
- Cluster Bus 通信正常。
- 客户端能够访问新主节点地址。
- 客户端能够及时刷新槽表。
需要同时评估:
RTO
恢复时间受以下因素影响:
cluster-node-timeout
+ FAIL 传播
+ 副本选举
+ 拓扑传播
+ 客户端重试
cluster-node-timeout 过大,故障发现慢;过小则容易因短暂抖动误判,并增加节点间心跳压力。
RPO
由于异步复制,RPO 不是严格为零。复制延迟越大,主节点故障后可能丢失的数据窗口越大。
故障域
如果主节点和它的副本位于同一台宿主机、同一机架或同一可用区,那么局部基础设施故障可能同时摧毁主副本。
Redis Cluster 的副本迁移机制能够把有多余副本的 Shard 中的一部分副本迁移给没有副本保护的主节点,从而改善后续故障承受能力,但它不能替代正确的初始故障域规划。(Redis)
8. 常见错误与生产事故
事故一:迁移到 Cluster 后大量 CROSSSLOT
现象
原来单机 Redis 中正常运行的 MGET、事务或 Lua 脚本,在 Cluster 中频繁返回 CROSSSLOT。
根因
Key 在设计时没有考虑同槽约束,例如:
user:42:profile
quota:42
两个 Key 的业务实体相同,但 Hash Slot 不同。
排查
CLUSTER KEYSLOT user:42:profile
CLUSTER KEYSLOT quota:42
同时梳理事务、Lua、WATCH 和多 Key 命令的 Key 列表。
修复
改为:
user:{42}:profile
user:{42}:quota
旧数据通过双写、回填、灰度读切换完成迁移。
预防
在 Key 设计评审中明确业务原子边界,并为所有多 Key 操作增加槽位测试。
事故二:Hash Tag 过粗导致单节点打满
现象
集群整体 CPU 只有 30%,某个主节点却达到 100%,延迟持续升高。
根因
大量 Key 使用相同 Tag:
cache:{prod}:...
所有生产缓存都进入同一槽。
排查
- 使用
CLUSTER KEYSLOT抽样。 - 查看各主节点 CPU、网络和命令量。
- Redis 8.2+ 使用
CLUSTER SLOT-STATS。 - 搜索 Key 命名中高度重复的
{...}。
修复
按用户、订单、子租户或业务分桶重新设计 Tag。
预防
禁止全局常量 Tag;上线前对真实 Key 样本进行槽分布统计。
事故三:MOVED 指向客户端不可达地址
现象
连接 Seed 节点成功,但执行部分 Key 时超时;日志中先出现 MOVED,随后出现连接拒绝或 DNS 错误。
根因
节点向客户端公告了容器内部 IP、错误端口或不可解析主机名。
排查
- 直接查看
MOVED返回地址。 - 查看
CLUSTER SHARDS或CLUSTER SLOTS。 - 从应用网络环境测试所有节点端口。
- 检查节点公告地址、NAT 和端口映射。
修复
配置客户端可达的节点公告地址和端口,确保数据端口及节点间 Cluster Bus 端口连通。
预防
部署验收时从真实应用网络逐个验证所有节点,而不是只验证入口地址。
事故四:迁槽过程中延迟尖刺
现象
扩容时 P99、P999 延迟明显升高,部分命令超时。
根因
待迁槽中存在 BigKey,MIGRATE 需要序列化并通过网络传输大量数据;同时 ASK 增加额外请求路径。
排查
- 在迁移前扫描 BigKey。
- 对比迁移槽的 Key 数和总字节数。
- 查看源、目标节点 CPU、网络和慢日志。
- 查看客户端
ASK、超时和重试数量。
修复
暂停或降低迁移速度,先拆分 BigKey,在低峰期按小批次迁移。
预防
建立 BigKey 准入和持续扫描机制;扩容容量预留应早于节点耗尽。
事故五:槽数均衡但内存和 QPS 严重不均
现象
每个主节点负责的槽数大致相等,但部分节点内存、CPU或网络远高于其他节点。
根因
槽数只反映所有权数量,不反映:
- 槽内 Key 数。
- Value 字节数。
- 访问频率。
- 命令复杂度。
- 热 Key 分布。
排查
联合分析节点指标、Key 大小、热点 Key、槽级 CPU、内存和网络指标。
修复
按实际负载迁移热点槽;必要时改变业务分片键,而不只是重新平均槽数。
预防
容量模型同时使用槽数、Key 数、字节数、QPS、带宽和命令 CPU。
事故六:故障转移时出现重试风暴和重复写
现象
主节点故障后,应用请求量激增;某些计数、扣减或队列写入发生重复。
根因
- SDK 自动重试。
- 业务 RPC 层再次重试。
- 网关又进行重试。
- 非幂等命令在响应丢失后被重新执行。
- 重试没有退避和总预算。
排查
按请求 ID 串联调用链,统计每层重试次数,对比 Redis 实际写入次数。
修复
- 只保留一层受控重试。
- 对非幂等操作使用幂等 Token。
- 使用 Lua 或数据库记录已处理请求。
- 增加指数退避和随机抖动。
预防
明确区分“命令确定未执行”和“执行结果未知”。网络超时通常属于后者。
事故七:开启副本读取后出现读己之写失败
现象
写入成功后立即读取,有时读到旧值或不存在。
根因
启用了 ReadOnly、RouteByLatency 或 RouteRandomly,读请求被发送到尚未追平的副本。
排查
检查客户端 ClusterOptions,查看读命令最终命中的节点及复制延迟。
修复
需要 Read-After-Write 的请求固定读取主节点;只有允许陈旧数据的读流量才下放副本。
预防
按业务语义配置客户端,不要为了“读扩展”全局开启副本读取。
事故八:主副本位于同一故障域
现象
一台宿主机或一个可用区故障后,同一 Shard 的主节点和副本同时离线,相关槽无法服务。
根因
只检查了主从数量,没有检查物理部署位置。
排查
建立 Node ID、主从关系、宿主机、机架、可用区的对应表。
修复
重新分配副本,使主副本跨宿主机、机架或可用区。
预防
将故障域分散作为部署和变更的强制校验项。
9. 方案选型与权衡
9.1 部署模式比较
| 方案 | 水平分片 | 自动故障转移 | 多 Key 限制 | 一致性 | 运维复杂度 | 适用场景 |
|---|---|---|---|---|---|---|
| 单机 Redis | 否 | 否 | 无 Cluster 槽限制 | 单节点语义 | 低 | 开发、小数据、可重建缓存 |
| 主从复制 | 否 | 通常需人工 | 无 Cluster 槽限制 | 异步复制 | 较低 | 读扩展、人工切换 |
| Sentinel | 否 | 是 | 无 Cluster 槽限制 | 异步复制 | 中 | 单主容量足够,需要自动 HA |
| Redis Cluster | 是 | 是 | 多 Key 必须同槽 | 非强一致 | 高 | 大容量、高吞吐、可自然分片 |
| 客户端一致性哈希 | 是 | 由应用实现 | 由应用决定 | 由各节点决定 | 高 | 特殊缓存分片、定制路由 |
| 代理或托管分片 | 通常是 | 取决于产品 | 取决于实现 | 取决于产品 | 应用侧较低 | 希望屏蔽拓扑和运维细节 |
| 关系/共识数据库 | 通常支持 | 通常支持 | 可支持跨分片事务 | 可提供更强保证 | 较高 | 权威数据、资金、严格事务 |
选择 Redis Cluster 的前提通常包括:
- 单主节点容量或吞吐已无法满足要求。
- 数据能够按用户、订单、设备、租户等自然分区。
- 主要操作可以局限在单个业务实体。
- 能接受副本异步复制和非零 RPO。
- 应用和运维团队能够处理拓扑、迁槽和热点问题。
以下情况不要因为“Redis 很快”而强行选择 Cluster:
- 强依赖跨分片事务。
- 权威账本不能接受已确认写入丢失。
- 主要负载集中在一个全局 Key。
- 数据访问模式无法形成稳定分片键。
- 团队无法维护 Cluster-Aware Client 和节点网络拓扑。
9.2 迁槽方式比较
| 维度 | 经典 MIGRATING/IMPORTING | Redis 8.4+ CLUSTER MIGRATION |
|---|---|---|
| 兼容性 | Redis 3.0+ 广泛适用 | 要求 Redis 8.4+ |
| 编排 | 需要管理多个槽状态和 MIGRATE | 目标节点发起迁移任务 |
| 请求路由 | 迁移期间常见 ASK | 支持新的原子交接机制 |
| 运维复杂度 | 较高 | 相对集中 |
| 数据复制 | 按 Key 迁移 | 后台迁移槽范围 |
| 风险 | BigKey、长 ASK 窗口、状态不一致 | 后台复制压力、交接停写、版本兼容 |
| 可观测性 | 依赖 redis-cli 和槽状态 | 提供任务 ID、STATUS、CANCEL |
即使使用新迁移机制,也必须进行容量评估、限速、低峰执行和失败演练。
10. 高频面试题
问题
1. Redis Cluster 为什么使用 16384 个槽?
推荐回答
30 秒回答: Redis Cluster 先把 Key 映射到固定的 16384 个槽,再把槽分配给节点。固定槽位便于客户端缓存、故障转移和在线迁移。16384 提供了足够的迁移粒度,同时槽位位图只有约 2KB,控制了心跳元数据开销。
展开来说,公式是 CRC16(key) mod 16384。16384 等于 2^14,节点通过槽位位图传播自己负责的槽。槽数太少会导致迁移粒度粗,槽数太多又会增加元数据和运维成本。(Redis)
面试官追问
- 为什么不直接把 Key 哈希到节点?
- 16384 个槽是否意味着最多只能存 16384 个 Key?
- 槽位均匀是否代表负载均匀?
常见错误回答
“因为 Redis 作者随便选了 16384”或“一个槽只能放一个 Key”。
评分点
初级说出公式;中级说明槽是中间层;高级能分析位图、迁移粒度和负载不均问题。
问题
2. Redis Cluster 为什么不用一致性哈希?
推荐回答
30 秒回答: Redis Cluster 使用固定槽位,把 key → slot 与 slot → node 分开。节点增减时只需要显式迁移槽位,不改变 Key 的槽号,客户端也只需维护固定范围的槽表。
一致性哈希适合节点环和客户端分片,但 Redis Cluster 更强调明确的槽位所有权、可控迁移、故障转移和 Hash Tag 共置能力。两者都是分片方案,并非一致性哈希一定更差。(Redis)
面试官追问
- 一致性哈希中的虚拟节点解决什么问题?
- 扩容时槽位和 Key 分别发生什么变化?
- Redis Cluster 扩容是否完全无开销?
常见错误回答
“一致性哈希扩容时会迁移全部数据,所以 Redis 不用。”
评分点
能比较环、虚拟节点、固定槽位和显式迁移,而不是简单否定一致性哈希。
问题
3. Hash Tag 是什么?有什么风险?
推荐回答
30 秒回答: Hash Tag 是 Key 中第一对有效 {...} 之间的内容。Redis 只对这段内容计算槽位,因此多个 Key 可以被强制放在同一槽,满足事务、Lua 和多 Key 命令要求。
风险是 Tag 粒度过粗会把大量数据和流量集中到一个槽。例如所有 Key 都使用 {prod},就会形成单槽热点。(Redis)
面试官追问
foo{}{bar}使用哪部分计算?- 用户 ID 本身带大括号怎么办?
- 同槽是否意味着同一个 Key?
常见错误回答
“只要 Key 中有大括号,Redis 就会取最后一对大括号。”
评分点
掌握第一对有效括号规则、业务原子边界和热点风险。
问题
4. MOVED 和 ASK 有什么区别?
推荐回答
30 秒回答: MOVED 表示槽位归属已经改变,客户端应把后续请求发到新节点并刷新槽表;ASK 表示槽正在迁移,只把当前请求临时发到目标节点,并在同一连接上先发送 ASKING,不能永久修改槽表。
ASK 的存在是因为同一槽中的一部分 Key 可能已经迁走,另一部分仍在源节点。(Redis)
面试官追问
- 为什么 ASKING 必须与原命令使用同一连接?
- 客户端收到 MOVED 后只修改一个槽够不够?
- 哪个错误通常出现在故障转移后?
常见错误回答
“ASK 和 MOVED 都是让客户端以后永远访问新节点。”
评分点
能说清永久与临时、槽表更新和 ASKING 一次性语义。
问题
5. PFAIL 和 FAIL 有什么区别?
推荐回答
30 秒回答: PFAIL 是单个节点在超过 NODE_TIMEOUT 后形成的本地故障怀疑,不足以触发故障转移;FAIL 是收集到多数主节点在有效窗口内的故障报告后形成的集群确认,并会广播给其他节点。
这种设计避免单节点网络抖动直接导致主从切换,但也意味着故障状态传播需要时间。(Redis)
面试官追问
- 副本能否把其他节点标记为 PFAIL?
- 为什么不能看到一次超时就故障转移?
- 这里的多数派是不是业务数据共识?
常见错误回答
“PFAIL 是副本故障,FAIL 是主节点故障。”
评分点
区分本地怀疑和多数派确认,并明确它不是业务写入共识。
问题
6. Redis Cluster 的故障转移过程是什么?
推荐回答
30 秒回答: 主节点先由 PFAIL 升级为 FAIL,符合新鲜度要求的副本根据复制偏移量排序并延迟发起选举。副本增加 Epoch,向主节点请求投票,获得多数票后晋升,取得更高 configEpoch,随后宣布接管原主节点的槽。
旧主恢复后看到更高 Epoch,通常会成为新主的副本。(Redis)
面试官追问
- 为什么最新副本优先?
- 为什么选举需要多数主节点投票?
- RTO 是否等于
cluster-node-timeout?
常见错误回答
“主节点断开后,任意副本会立刻自动变主。”
评分点
覆盖 FAIL、候选资格、Rank、投票、Epoch、槽位传播和客户端刷新。
问题
7. 多数主节点不可达时会发生什么?
推荐回答
30 秒回答: Redis Cluster 的自动故障转移需要多数主节点投票。若多数主节点不可达,就无法安全完成选举,集群通常进入失败状态;默认配置下节点会停止提供流量。
如果只是少数主节点故障,并且每个故障主节点都有可用副本,多数派一侧可以完成故障转移。cluster-require-full-coverage 和 cluster-allow-reads-when-down 可以改变部分行为,但不会创造不存在的多数派。(Redis)
面试官追问
- 三主三从最多能容忍几个节点故障?
- 为什么不能只按节点总数判断?
- 主副本同时故障会怎样?
常见错误回答
“只要还有一个 Redis 节点存活,Cluster 就能继续工作。”
评分点
按主节点多数派、Shard 副本覆盖和故障域进行分析,而不是只数存活节点。
问题
8. Redis Cluster 为什么会丢失已经确认的写入?
推荐回答
30 秒回答: 因为主从复制默认是异步的。主节点可以先返回 OK,再把命令传播给副本。如果返回后立即故障,尚未收到该写入的副本被提升为主,写入就会丢失。
网络分区时,少数派旧主在有限窗口内接收的写入,也可能在多数派完成故障转移后被覆盖。WAIT 能降低概率,但不提供严格强一致性。(Redis)
面试官追问
- AOF 是否能彻底解决这个问题?
WAIT 1 1000保证了什么?- RPO 与复制延迟有什么关系?
常见错误回答
“只要开启 AOF everysec,Cluster 就绝不会丢数据。”
评分点
能够区分复制、持久化、客户端确认和故障选举。
问题
9. Cluster 中多 Key、事务和 Lua 有什么限制?
推荐回答
30 秒回答: 一次服务端原子执行涉及的所有 Key 必须属于同一 Hash Slot,包括多 Key 命令、事务、WATCH 和 Lua。通常通过 Hash Tag 将同一业务实体的 Key 放入同槽。
普通 Pipeline 可以被客户端拆分到多个节点,但它只是网络优化,不能提供跨节点原子性。(Redis)
面试官追问
- 为什么普通 Pipeline 可以跨槽?
- TxPipeline 为什么不能跨槽?
- 如何设计用户余额和流水 Key?
常见错误回答
“使用 Pipeline 后,多节点命令就自动成为事务。”
评分点
区分命令原子性、事务、Lua、Pipeline 和分布式事务。
问题
10. Redis Cluster 如何在线扩容?
推荐回答
30 秒回答: 先将空节点加入集群,再从已有主节点迁移部分槽到新节点。经典流程将目标槽设为 IMPORTING、源槽设为 MIGRATING,逐批 MIGRATE Key,最后通过 SETSLOT NODE 切换槽位所有权。
客户端在迁移期间通过 ASK 访问已经迁走的 Key。Redis 8.4+ 还提供 CLUSTER MIGRATION 原子槽迁移任务。(Redis)
面试官追问
- 为什么新节点加入后不会自动分担数据?
- 迁槽过程中请求为什么还能继续?
- BigKey 对扩容有什么影响?
常见错误回答
“把新节点加进 Cluster 后,Redis 会立即自动平均所有数据。”
评分点
覆盖加节点、迁槽状态、ASK、所有权切换、检查和 BigKey 风险。
问题
11. 如何发现和处理热点槽?
推荐回答
30 秒回答: 不能只比较每个节点的槽数,应同时观察槽内 Key 数、内存、CPU、网络和访问频率。热点通常来自过粗 Hash Tag、HotKey、BigKey、超大租户或全局单 Key。
Redis 8.2+ 可使用 CLUSTER SLOT-STATS;旧版本可以结合节点指标、命令统计、HotKey 分析和 CLUSTER KEYSLOT 抽样。(Redis)
面试官追问
- 迁移热点槽一定能解决 HotKey 吗?
- 单个全局排行榜如何拆分?
- 槽位数量均衡为什么可能没有意义?
常见错误回答
“每个主节点分配相同数量的槽就一定均衡。”
评分点
能区分槽热点、Key 热点、内存倾斜和业务模型问题。
问题
12. go-redis 的 ClusterClient 如何处理路由和批量请求?
推荐回答
30 秒回答: redis.NewClusterClient 使用 Seed 节点建立槽位拓扑,根据 Key 计算槽位并访问目标节点,内部处理 MOVED、ASK 和有限重试。客户端可被多个 Goroutine 复用,连接池按节点建立。
普通 Pipeline 可以按目标节点拆分并并发执行,因此可能部分成功;TxPipeline 使用 MULTI/EXEC,带 Key 的命令必须同槽。业务不能把跨槽 Pipeline 当成分布式事务。(Redis)
面试官追问
PoolSize是整个集群还是每个节点?- 网络超时后为什么不能总是安全重试
INCR? - 什么时候可以开启
RouteByLatency?
常见错误回答
“ClusterClient 只维护一条连接,并把所有请求发送给 Seed 节点。”
评分点
覆盖槽位路由、重定向、并发安全、每节点连接池、Pipeline 部分成功和幂等性。
11. 一分钟面试回答
Redis Cluster 同时提供数据分片和分片级高可用。它把 Key 通过 CRC16 mod 16384 映射到固定槽,再把槽分配给主节点;节点变化时迁移的是槽位及其 Key,而不是改变 Key 的槽号。客户端缓存槽到节点的映射,MOVED 表示槽位永久变化,ASK 表示迁槽期间只临时重定向当前命令。节点通过 Cluster Bus 和 Gossip 传播拓扑,超时后先形成本地 PFAIL,多数主节点确认后形成 FAIL,副本获得多数票并用更高 configEpoch 接管槽位。Cluster 使用异步复制,因此不是强一致系统,已经返回 OK 的写入仍可能在故障转移中丢失。多 Key 命令、事务和 Lua 必须同槽,通常使用 Hash Tag。扩容本质是加入空节点并迁移槽;性能治理不能只看槽数,还要看热点 Key、内存、CPU和网络。
12. 本章总结
- Redis Cluster 的核心映射是
Key → Slot → Master。 - 16384 个固定槽为路由、故障转移和在线迁移提供了稳定中间层。
- Hash Tag 能实现同槽操作,但过度使用会造成热点槽。
MOVED表示永久拓扑变化,ASK表示迁槽期间的单次临时重定向。PFAIL是本地怀疑,FAIL是多数主节点报告后的故障确认。- 故障转移依赖副本新鲜度、多数投票和更高
configEpoch。 - 多数主节点不可达时,Cluster 通常无法继续安全选举并进入失败状态。
- 异步复制使 Redis Cluster 具有非零 RPO,
WAIT只能降低风险。 - 多 Key、事务和 Lua 必须同槽;跨槽 Pipeline 不具备原子性。
- 扩容和缩容的本质是槽位迁移,迁移成本取决于实际 Key 和字节数。
- 槽数均衡不等于负载均衡,必须治理 HotKey、BigKey 和不合理 Hash Tag。
- go-redis ClusterClient 应全局复用,并对超时、重试、部分成功和副本旧读进行业务级控制。
13. 自测清单
user:{100}:profile和order:{100}:detail是否一定同槽?为什么?- 为什么收到
ASK后不能永久修改本地槽位表? PFAIL为什么不足以直接触发副本晋升?- 三主三从中,两个主节点所在故障域同时完全离线时会发生什么?
- 为什么
WAIT不能把 Redis Cluster 变成严格强一致系统? - 普通 Pipeline 跨三个主节点执行时,可能出现哪些部分成功状态?
- 一个节点槽数最少,为什么它的内存仍可能最高?
- 缩容删除主节点前必须完成哪些步骤?
RouteByLatency对 Read-After-Write 语义有什么影响?- 如何为超大租户设计 Hash Tag,既支持局部原子操作,又避免单槽热点?